/**
 * 
 */
package truerefactor.refactor;

import java.util.ArrayList;
import java.util.List;

import truerefactor.graph.AttributeNode;
import truerefactor.graph.ClassNode;
import truerefactor.graph.CodeGraph;
import truerefactor.graph.CodeNode;
import truerefactor.graph.Edge;
import truerefactor.graph.EdgeType;
import truerefactor.graph.MethodNode;

/**
 * PushDownMethod - An implementation of the Push Down Method Refactoring
 * Technique
 * 
 * @author Isaac Griffith
 */
public class PushDownMethod extends Refactoring {

    /**
     * Creates a new PushDownMethod instance
     */
    public PushDownMethod()
    {
        this.name = "Push-down Method";
        this.description = "";
    }

    /*
     * (non-Javadoc)
     * @see
     * truerefactor.refactor.Refactoring#execute(truerefactor.graph.CodeNode [])
     */
    @Override
    public void execute(CodeGraph graph)
    {
        if (operands.isEmpty())
            return;

        MethodNode method = (MethodNode) operands.get(0);
        if (method.getParentClass() == null)
            return;

        ClassNode parent = method.getParentClass();
        List<ClassNode> subClasses = parent.getSubClasses();

        declareMethod(graph, method, subClasses);

        removeUnusedMethods(graph, method, subClasses);

    }

    private void removeUnusedMethods(CodeGraph graph, MethodNode method, List<ClassNode> subClasses)
    {
        // 3. Remove the method from each subclass that does not need it.
        List<MethodNode> otherMethods = method.getOtherMethods();
        List<ClassNode> otherClasses = new ArrayList<ClassNode>();
        for (MethodNode mNode : otherMethods)
        {
            ClassNode temp = mNode.getParentClass();
            if (subClasses.contains(temp))
            {
                otherClasses.add(temp);
            }
        }

        List<CodeNode> remList = new ArrayList<CodeNode>();
        remList.addAll(subClasses);

        remList.removeAll(otherClasses);

        for (CodeNode node : remList)
        {
            if (node instanceof ClassNode)
            {
                ClassNode temp = (ClassNode) node;

                if (!graph.isEdge(method, temp, EdgeType.Method) || !graph.isEdge(temp, method, EdgeType.Method))
                {
                    List<MethodNode> methods = temp.getMethodsByName(method.getIdentifier());
                    for (MethodNode mNode : methods)
                    {
                        if (mNode.getIdentifier().equals(method.getIdentifier()))
                        {
                            if (mNode.getReturnType().equals(method.getReturnType()))
                            {
                                if (mNode.getParameters().equals(method.getParameters()))
                                {
                                    mNode.getParentClass().removeMethod(mNode);
                                    graph.remove(graph.getEdge(mNode.getParentClass(), mNode, EdgeType.Method));
                                    graph.remove(mNode);
                                }
                            }
                        }
                    }
                }
            }
        }

        // 2. Remove method from superclass
        // a. You may have to change callers to use the subclass in variable and
        // parameter declarations
        // b. IF it makes sense to access the method through a superclass
        // variable, you don't intend to remove the method from any subclasses,
        // and the superclass is abstract, you can declare the method as
        // abstract, in the superclass.
        graph.remove(method);
    }

    private void declareMethod(CodeGraph graph, MethodNode method, List<ClassNode> subClasses)
    {
        // 1. Declare a method in all subclasses and copy the body into each
        // subclass
        // a. You may need to declare fields as protected for the method to
        // access them. Usually you do this if you intend to push down the field
        // later. Otherwise use an accessor on the superclass. If this accessor
        // is not public, you need to declare it as public.

        for (ClassNode node : subClasses)
        {
            MethodNode mNode = new MethodNode(method.getIdentifier(), method.getCode(), method.getAccessibility());

            graph.cloneStatementGraph(mNode, method.getStatementGraph());

            graph.insert(mNode);
            graph.insert(new Edge(node, mNode, EdgeType.Method));
            mNode.setParentClass(node);
            node.addMethod(mNode);

            List<MethodNode> otherMethods = method.getOtherMethods();
            List<AttributeNode> otherAttributes = method.getOtherAttributes();

            for (MethodNode other : otherMethods)
            {
                if (mNode.getParentClass().equals(other.getParentClass()))
                {
                    other.removeOtherMethod(method);
                    other.addAssociatedMethod(mNode);
                    mNode.addAssociatedMethod(other);
                }
            }

            for (AttributeNode other : otherAttributes)
            {
                if (mNode.getParentClass().equals(other.getParent()))
                {
                    other.removeAssociatedOtherMethod(method);
                    other.addAssociatedClassMethod(mNode);
                    mNode.addAssociatedAttribute(other);
                }
            }
        }
    }
}
